package org.noworks.android.mvc.activities;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.noworks.android.mvc.MvcApplication;
import org.noworks.android.mvc.R;
import org.noworks.android.mvc.annotations.ActionMethod;
import org.noworks.android.mvc.annotations.Controller;
import org.noworks.android.mvc.exceptions.ActionMethodMappingOverlapException;
import org.noworks.android.mvc.exceptions.ActionNotFoundException;

import android.app.Application;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;

public abstract class MvcFragmentActivity extends FragmentActivity {

	private MvcApplication application; // Current MvcApplication

	private Map<String, List<Method>> methods; // Methods mapped to their action
												// name
	private String lastAction; // Last action that the controller performed

	/**
	 * @param action
	 *            ActionMethod to call
	 * @throws ActionNotFoundException
	 *             Thrown when their is no ActionMethod for the given action
	 * @throws IllegalArgumentException
	 *             Thrown when their is no zero parameter method for the action
	 *             to be performed
	 */
	public final void action(String action) throws ActionNotFoundException,
			IllegalArgumentException {
		List<Method> methodsList = methods.get(action);
		if (methodsList == null)
			throw new ActionNotFoundException("No action found for: " + action
					+ "()");
		Method method = grabProperMethod(methodsList);
		if (method == null)
			throw new ActionNotFoundException(
					"No action found with no arguments for: " + action + "()");
		try {
			method.invoke(this);
			setLastAction(action);
		} catch (IllegalArgumentException e) {
			throw e;
		} catch (IllegalAccessException e) {
			/*
			 * Shouldn't happen as the methods are set accessible before being
			 * put into the map
			 */
		} catch (InvocationTargetException e) {
			/*
			 * Thrown when there is an unhandled exception inside of the called
			 * method, if you're handling exceptions properly this shouldn't be
			 * an issue
			 */
			throw new RuntimeException(e);
		}
	}

	/**
	 * @param controller
	 *            Controller to call the ActionMethod in
	 * @param action
	 *            ActionMethod to call
	 */
	public final void action(String controller, String action) {
		if (equalsControllerName(controller))
			action(action);
		application.action(controller, action);
	}

	/**
	 * @param action
	 *            ActionMethod to call
	 * @param args
	 *            Arguments to be passed to the ActionMethod
	 */
	public final void actionArguments(String action, Object... args) {
		List<Method> methodList = methods.get(action);
		if (methodList == null) {
			StringBuilder message = new StringBuilder("No action found for: ");
			message.append(action);
			message.append('(');
			for (Object obj : args)
				message.append(obj.getClass().getCanonicalName());
			message.append(')');
			throw new ActionNotFoundException(message.toString());
		}
		Method method = grabProperMethod(methodList, args);
		if (method == null) {
			StringBuilder message = new StringBuilder("No action found for: ");
			message.append(action);
			message.append('(');
			for (Object obj : args)
				message.append(obj.getClass().getCanonicalName());
			message.append(')');
			throw new ActionNotFoundException(message.toString());
		}
		try {
			Class<?> returnClass = method.getReturnType();
			if (returnClass != null
					&& Fragment.class.isAssignableFrom(returnClass))
				setFragment(((Fragment) method.invoke(this, args)), true);
			else
				method.invoke(this, args);
			setLastAction(action);
		} catch (IllegalArgumentException e) {
			throw e;
		} catch (IllegalAccessException e) {
			/*
			 * Shouldn't happen as the methods are set accessible before being
			 * put into the map
			 */
		} catch (InvocationTargetException e) {
			/*
			 * Thrown when there is an unhandled exception inside of the called
			 * method, if you're handling exceptions properly this shouldn't be
			 * an issue
			 */
			throw new RuntimeException(e);
		}
	}

	/**
	 * @param controller
	 *            Controller to call the ActionMethod in
	 * @param action
	 *            ActionMethod to call
	 * @param args
	 *            Arguments to be passed to the ActionMethod
	 */
	protected final void actionArguments(String controller, String action,
			Object... args) {
		if (equalsControllerName(controller))
			action(action);
		application.actionArguments(controller, action, args);
	}

	private Class<?>[] argsToTypes(Object... args) {
		Class<?>[] types = new Class<?>[args.length];
		for (int idx = 0; idx < args.length; idx++)
			if (args[idx] != null)
				types[idx] = args[idx].getClass();
		return types;
	}

	private boolean checkForSameArgs(List<Method> methodList, Method newMethod) {
		boolean state = false;
		for (Method method : methodList) {
			Class<?>[] methodParams = method.getParameterTypes();
			Class<?>[] newMethodParams = newMethod.getParameterTypes();
			boolean typeargs = compareTypeArgs(methodParams, newMethodParams);
			if (typeargs)
				state = true;
		}
		return state;
	}

	private boolean checkIfMethodExistsForArgs(Method method, Object... args) {
		Class<?>[] methodParams = method.getParameterTypes();
		Class<?>[] argTypes = argsToTypes(args);
		boolean sameTypes = compareTypeArgs(methodParams, argTypes);
		return sameTypes;
	}

	private boolean compareTypeArgs(Class<?>[] methodParams,
			Class<?>[] argParams) {
		boolean state = true;
		if (methodParams.length != argParams.length)
			return false;
		if (methodParams.length == 0 && argParams.length == 0)
			return true;
		for (int idx = 0; idx < argParams.length; idx++) {
			Class<?> methodParam = methodParams[idx];
			Class<?> argParam = argParams[idx];
			if (methodParam == null || argParam == null)
				state = false;
			/* methodParam is not a superclass or superinterface of methodParam */
			if (!methodParam.isAssignableFrom(argParam))
				state = false;
		}
		return state;
	}

	/**
	 * Checks to see if the controller name matches
	 * 
	 * @param controllerName
	 *            Controller name to match against
	 * @return
	 */
	public final boolean equalsControllerName(String controllerName) {
		return controllerName.equals(getControllerName());
	}

	/**
	 * Get the controllers name based on the Controller annotation, defaults to
	 * the controllers simple name
	 * 
	 * @return The controller name used in the controller mapping
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public final String getControllerName() {
		Class clazz = getClass();
		String controllerName = clazz.getSimpleName();
		if (clazz.isAnnotationPresent(Controller.class)) {
			Controller ca = (Controller) clazz.getAnnotation(Controller.class);
			if (!ca.value().equals(""))
				controllerName = ca.value();
		}
		return controllerName;
	}

	protected final Fragment getFragment() {
		return getSupportFragmentManager().findFragmentById(
				R.id.fragment_container);
	}

	/**
	 * Returns the last action performed by this controller
	 * 
	 * @return Last action performed
	 */
	protected final String getLastAction() {
		return lastAction;
	}

	/**
	 * Gets the last action that the application performed
	 * 
	 * @return Last performed action
	 */
	protected final String getLastApplicationAction() {
		return application.getLastAction();
	}

	/**
	 * Convenience method to get the current MvcApplication
	 * 
	 * @return MvcApplication
	 */
	public MvcApplication getMvcApplication() {
		return application;
	}

	private final Method grabProperMethod(List<Method> methods) {
		for (Method method : methods) {
			if (method.getParameterTypes().length == 0)
				return method;
		}
		return null;
	}

	private final Method grabProperMethod(List<Method> methods, Object... args) {
		for (Method method : methods) {
			boolean sameTypes = checkIfMethodExistsForArgs(method, args);
			if (sameTypes)
				return method;
		}
		return null;
	}

	/**
	 * Initializes the method mapping for the controller and sets all actions to
	 * accessible to prevent conflict later on in the routing methods
	 */
	private final void initMethodMap() {
		Class<? extends MvcFragmentActivity> clazz = getClass();
		methods = new HashMap<String, List<Method>>();
		Method[] classMethods = clazz.getDeclaredMethods();
		for (Method method : classMethods) {
			if (method.isAnnotationPresent(ActionMethod.class)) {
				String methodName = method.getName();
				ActionMethod am = method.getAnnotation(ActionMethod.class);
				if (!am.value().equals(""))
					methodName = am.value();
				List<Method> methodList = methods.get(methodName);
				if (methodList == null) {
					methodList = new ArrayList<Method>();
					methodList.add(method);
					methods.put(methodName, methodList);
				} else {
					if (checkForSameArgs(methodList, method))
						throw new ActionMethodMappingOverlapException(
								"ActionMethod already mapped for name: "
										+ methodName + " args: "
										+ method.getParameterTypes());
					else
						methodList.add(method);
				}
			}
		}
	}

	@Override
	protected void onCreate(Bundle arg0) {
		super.onCreate(arg0);

		Application app = getApplication();
		if (app instanceof MvcApplication)
			application = (MvcApplication) app;

		application.registerController(getControllerName(), this);

		initMethodMap();
	}

	protected final void setFragment(Fragment fragment) {
		setFragment(fragment, false);
	}

	protected final void setFragment(Fragment fragment, boolean stack) {
		FragmentTransaction trans = getSupportFragmentManager()
				.beginTransaction();
		trans.replace(R.id.fragment_container, fragment);
		if (stack)
			trans.addToBackStack(null);
		trans.commit();
	}

	/**
	 * Sets the last action performed
	 * 
	 * @param lastAction
	 *            Last action performed
	 */
	private final void setLastAction(String lastAction) {
		this.lastAction = lastAction;
		application.setLastAction(lastAction);
	}

}
